8-1 菜单权限:需求分析+创建数据库模型+REST接口
本节进入菜单权限模块的开发。菜单权限与 RBAC 接口权限和策略数据权限有本质区别:它不需要对字段级别进行精细控制,而是根据角色分配不同的菜单数据,前端据此动态展示用户可见的页面。
一、三种权限控制对比
| 维度 | 接口权限(RBAC) | 策略权限(Policy) | 菜单权限(Menu) |
|---|---|---|---|
| 控制粒度 | 接口级别 | 资源实例+字段级别 | 页面级别 |
| 判断方式 | 角色是否拥有权限 | 用户能否对资源执行操作 | 角色可访问哪些菜单 |
| 安全侧重点 | 后端为主 | 后端为主 | 前端展示,后端保障 |
| 典型场景 | 管理后台 API 访问控制 | 只能编辑自己创建的文章 | 侧边栏菜单动态渲染 |
重要提示:菜单权限的安全性要求相对较低,前端控制的是 UI 展示,真正的安全保障仍然依赖后端的接口权限和策略权限。一个程序的安全性主要靠后端保障,而非仅靠前端隐藏菜单。
二、菜单权限的架构设计
菜单权限同样采用三层架构:
用户(User) → 角色(Role) → 菜单(Menu)
text
中间通过角色层来划分不同的菜单访问权限。关键挑战在于菜单的树形数据结构如何在关系型数据库中存储。
三、树形结构在关系型数据库中的存储方案
管理后台的菜单通常具有层级结构(一级、二级、三级甚至四级菜单),需要处理树形嵌套数据。
| 存储方式 | 适用数据库 | 说明 |
|---|---|---|
| 嵌套文档 | MongoDB | 天然支持嵌套数据 |
| 自引用(Self Relation) | PostgreSQL / MySQL | 通过 parentId 外键引用自身 |
在 PostgreSQL 中,Prisma 通过 Self Relation(自关联) 实现树形结构。
四、Prisma Schema 设计
菜单数据分为两个 Model:Menu(菜单主体)和 MenuMeta(路由元信息),采用两种关系类型:
1. Menu Model(菜单主体)
model Menu {
id Int @id @default(autoincrement())
name String @unique // 路由名称
path String // 路由路径(必传)
component String? // 组件路径
redirect String? // 重定向路径
fullPath String? // 完整路径
alias String? // 路由别名
// 一对一关系:Menu → MenuMeta
meta MenuMeta? // 路由元信息
// 自关联:树形结构
parentId Int? // 父级菜单 ID
parent Menu? @relation("MenuChildren", fields: [parentId], references: [id])
children Menu[] @relation("MenuChildren")
// 多对多关系:Role → Menu(后续添加)
roleMenus RoleMenu[]
}
prisma
2. MenuMeta Model(路由元信息)
model MenuMeta {
id Int @id @default(autoincrement())
title String // 菜单标题
icon String? // 菜单图标
order Int @default(100) // 排序权重
hideMenu Boolean @default(false) // 是否隐藏菜单
disabled Boolean @default(false) // 是否禁用
// 一对一关系:MenuMeta → Menu
menuId Int @unique // 唯一外键,一个 Meta 只对应一个 Menu
menu Menu @relation(fields: [menuId], references: [id])
}
prisma
五、两种关系类型详解
1. One-to-One 关系(Menu ↔ MenuMeta)
Menu (1) ←→ (1) MenuMeta
text
menuId字段标记为@unique,确保一个 Menu 只能有一个 Meta- Menu 侧的
meta字段加?表示可选(创建时可能暂无 Meta)
2. Self Relation 关系(Menu → Menu)
Menu (parent) ←→ Menu[] (children)
text
这是 Prisma 中的**自关联(Self Relation)**固定写法:
// 子菜单侧:通过 parentId 引用父菜单
parentId Int?
parent Menu? @relation("MenuChildren", fields: [parentId], references: [id])
// 父菜单侧:通过关联名称获取所有子菜单
children Menu[] @relation("MenuChildren")
prisma
关键要点:
| 要点 | 说明 |
|---|---|
| 关联名称一致 | parent 和 children 的 @relation 名称必须相同("MenuChildren") |
| parentId 可选 | 一级菜单没有父级,parentId 为 null |
| 单一父级 | 一个菜单最多只有一个 parent |
| 多个子级 | 一个菜单可以有多个 children |
六、同步数据库并创建模块
1. 同步数据库 Schema
npx prisma db push
bash
该命令会将 Schema 变更同步到 PostgreSQL 数据库,同时重新生成 Prisma Client。
2. 使用 NestJS CLI 创建模块
nest g resource modules/menu --no-spec
bash
工程目录规范:业务模块统一放在
modules/目录下,而用户认证、数据库等基础模块放在外层。随着项目规模增长,保持清晰的目录结构至关重要。
七、创建 DTO
根据 Menu 模型的字段设计 CreateMenuDto:
// dto/create-menu.dto.ts
import { IsString, IsOptional, IsInt, IsBoolean, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
class CreateMenuMetaDto {
@IsString()
title: string;
@IsOptional()
@IsString()
icon?: string;
@IsOptional()
@IsInt()
order?: number;
@IsOptional()
@IsBoolean()
hideMenu?: boolean;
@IsOptional()
@IsBoolean()
disabled?: boolean;
}
export class CreateMenuDto {
@IsString()
name: string;
@IsString()
path: string;
@IsOptional()
@IsString()
component?: string;
@IsOptional()
@IsString()
redirect?: string;
@IsOptional()
@IsString()
fullPath?: string;
@IsOptional()
@IsString()
alias?: string;
@IsOptional()
@IsInt()
parentId?: number;
@IsOptional()
@ValidateNested()
@Type(() => CreateMenuMetaDto)
meta?: CreateMenuMetaDto;
}
typescript
八、总结
本节完成了菜单权限模块的需求分析和数据库建模,核心知识点包括:
| 知识点 | 说明 |
|---|---|
| 权限分类 | 菜单权限控制页面可见性,安全性低于接口/策略权限 |
| Self Relation | Prisma 中实现树形结构的标准方式 |
| One-to-One | Menu 和 MenuMeta 的一对一关系 |
| DTO 设计 | 支持嵌套的 Meta 数据验证 |
| 工程规范 | 业务模块放在 modules/ 目录下 |
下一节将基于此 Schema 实现菜单的 CRUD 操作,特别是嵌套数据的查询处理。
↑